//
//  NewAMIO.m
//  RunSketch
//
//  Created by Brian on 10/18/09.
//  Copyright 2009 Tellart. All rights reserved.
//

#import "NewAMIO.h"
#include <AudioUnit/AudioUnit.h>
#define kOutputBus 0
#define kInputBus 1

@implementation NewAMIO

@synthesize carrierSampleFile, enableCarrierTone, inputAvailable, recorder;

/*
static AMIOAmplitudeValue GetAmplitudeFromFrameValue(UInt32 fr)
{
    AMIOAmplitudeValue amp = (labs(fr) << 2) & 0x0000ffff;
    //float f = fabs((float)(labs(fr) - 2147450800.0) / 485000.0);
    //AMIOAmplitudeValue amp = (AMIOAmplitudeValue)floorf(f);
    ampDebugCount++;
    if (ampDebugCount == 100000) {
        printf("[");
        for (int i = 0; i < (fr / 100000); i++) {
            printf("*");
        }
        printf("]\n");
        //printf("fr = %x, amp = %x, amp = %i \n", fr, amp, amp);
        ampDebugCount = 0;
    }
    return amp;
}
*/

-(id)init
{
    if (self = [super init])
    {
        //set up the audio session
        [self _setupSession];
    }
    return self;
}

- (void)initAudio:(NSString*)pathToSample 
{
    if (initialized) return; //don't do this more than once!
    initialized = YES;
    
    [self _setupAV];
    [self _setupAU];
    [self _setupSample:pathToSample];
    
	[self.carrierSampleFile reset];
    [self start];
}

- (void)dealloc {

	AudioUnitUninitialize(outputAudioUnit);
    
    [carrierSampleFile release];
    [recorder release];
    
    [super dealloc];
}

- (void) _setupAV
{
    NSURL *url = [NSURL URLWithString:@"/dev/null"];
    NSDictionary *settings = [NSDictionary dictionaryWithObjectsAndKeys:
            [NSNumber numberWithFloat: 44100.0],                 AVSampleRateKey,
            [NSNumber numberWithInt: kAudioFormatAppleLossless], AVFormatIDKey,
            [NSNumber numberWithInt: 1],                         AVNumberOfChannelsKey,
            [NSNumber numberWithInt: AVAudioQualityMax],         AVEncoderAudioQualityKey,
          nil];

    NSError *error = [NSError alloc];
    AVAudioRecorder *r = [[AVAudioRecorder alloc]
        initWithURL:url
        settings:settings
        error:&error];

    if (r) {
            [r prepareToRecord];
            r.meteringEnabled = YES;
            [r record];
        } else
        NSLog(@"NewAMIO: _setupAV error: %@", [NSError description]);
    
    self.recorder = r;
    [r release];
    [url release];
}

- (AMIOAmplitudeValue)averagePowerForChannel:(NSUInteger)channelNumber
{
    [recorder updateMeters];
    return [recorder averagePowerForChannel:channelNumber];
}

- (AMIOAmplitudeValue)peakPowerForChannel:(NSUInteger)channelNumber
{
    [recorder updateMeters];
    return [recorder peakPowerForChannel:channelNumber];
}

- (AMIOAmplitudeValue)lowpassPowerForChannel:(int)channelNumber
{
    [recorder updateMeters];
    
    const double ALPHA = 0.05;
    double peakPower = pow(10, (0.05 * [recorder peakPowerForChannel:(NSUInteger)channelNumber]));
    lowpassLevels[channelNumber] = ALPHA * peakPower + (1.0 - ALPHA) * lowpassLevels[channelNumber];
    return lowpassLevels[channelNumber];
}

- (AMIOAmplitudeValue)midrangePowerForChannel:(int)channelNumber
{
    return [self lowpassPowerForChannel:channelNumber];
}

- (AMIOAmplitudeValue)highpassPowerForChannel:(int)channelNumber
{
    return [self lowpassPowerForChannel:channelNumber];
}


- (void) _setupSample:(NSString*)pathToSample
{
    NSLog(@"NewAMIO: _setupSample");
    
	carrierSampleFile = [[AMIOAudioSampleFile alloc]init];
	//open the a wav file from the application resources
	[carrierSampleFile open:pathToSample];
}


/* Parameters on entry to this function are :-
 
 *inRefCon - used to store whatever you want, can use it to pass in a reference to an objectiveC class
			 i do this below to get at the AMIOAudioSampleFile object, the line below :
				callbackStruct.inputProcRefCon = self;
			 in the initialiseAudio method sets this to "self" (i.e. this instantiation of NewAMIO).
			 This is a way to bridge between objectiveC and the straight C callback mechanism, another way
			 would be to use an "evil" global variable by just specifying one in theis file and setting it
			 to point to carrierSampleFile whenever it is set.
 
 *inTimeStamp - the sample time stamp, can use it to find out sample time (the sound card time), or the host time
 
 inBusnumber - the audio bus number, we are only using 1 so it is always 0 
 
 inNumberFrames - the number of frames we need to fill. In this example, because of the way outputAudioFormat is
				  initialised below, a frame is a 32 bit number, comprised of two signed 16 bit samples.
 
 *ioData - holds information about the number of audio buffers we need to fill as well as the audio buffers themselves */
static OSStatus auCallback(void *inRefCon, 
								 AudioUnitRenderActionFlags *ioActionFlags, 
								 const AudioTimeStamp *inTimeStamp, 
								 UInt32 inBusNumber, 
								 UInt32 inNumberFrames, 
								 AudioBufferList *ioData) {  
	NewAMIO *theNewAMIO = (NewAMIO *)inRefCon;

    //fill the output buffer with data from audio input (mic)
    BOOL renderedInputData = NO;
    if (theNewAMIO.inputAvailable) {

        renderedInputData = YES;
        /*
        OSStatus err = AudioUnitRender(
            outputAudioUnit,      //AudioUnit inUnit,
            ioActionFlags,      //AudioUnitRenderActionFlags *ioActionFlags,
            inTimeStamp,        //const AudioTimeStamp *inTimeStamp,
            kInputBus,          //UInt32 inOutputBusNumber,
            inNumberFrames,     //UInt32 inNumberFrames,
            ioData);            //AudioBufferList *ioData
        if (err) {
            if (err == kAudioUnitErr_CannotDoInCurrentContext) {
                //probably means no mic was connected
            } else {
                //some other error
                printf("NewAMIO auCallback: ERROR: %d\n", (int)err);
            }
            renderedInputData = NO;
        }
        */
    }
	
	//get a copy of the objectiveC class "self" we need this to get the next sample to fill the buffer
    //UInt32 maxamp = 0;
    //UInt32 amp = 0;
    //AMIOAmplitudeValue maxamp = 0;
    //AMIOAmplitudeValue amp = 0;
	
    
	//loop through all the buffers that need to be filled
	for (int i = 0 ; i < ioData->mNumberBuffers; i++){
		//get the buffer to be filled
		AudioBuffer buffer = ioData->mBuffers[i];

        //NSLog(@"auCallback - inBusNumber=%i buffer=%i", inBusNumber, i);
		
		//if needed we can get the number of bytes that will fill the buffer using
		// int numberOfSamples = ioData->mBuffers[i].mDataByteSize;
		
		//get the buffer and point to it as an UInt32 (as we will be filling it with 32 bit samples)
		//if we wanted we could grab it as a 16 bit and put in the samples for left and right seperately
		//but the loop below would be for(j = 0; j < inNumberFrames * 2; j++) as each frame is a 32 bit number
		UInt32 *frameBuffer = buffer.mData;
		
        if ([theNewAMIO enableCarrierTone]) {
            //NSLog(@"NewAMIO: auCallback - carrier tone");
            //replace the audio data with the carrier sample
            //loop through the buffer and fill the frames
            for (int j = 0; j < inNumberFrames; j++){
            
                //adjust the peak amplitude if necessary
                /*if (renderedInputData) {// && (inBusNumber == kOutputBus)) {
                    amp = GetAmplitudeFromFrameValue(frameBuffer[j]);
                    if (amp > maxamp) maxamp = amp;
                }*/
            
                // get NextPacket returns a 32 bit value, one frame.
                frameBuffer[j] = [[theNewAMIO carrierSampleFile] getNextPacket];
            }
        } else {
            //silence the data on output
            //NSLog(@"NewAMIO: auCallback - silence");
            for (int j = 0; j < inNumberFrames; j++){
            
                //adjust the peak amplitude if necessary
                /*
                if (renderedInputData) {// && (inBusNumber == kOutputBus)) {
                    amp = GetAmplitudeFromFrameValue(frameBuffer[j]);
                    if (amp > maxamp) maxamp = amp;
                }
                */
                
                // get NextPacket returns a 32 bit value, one frame.
                frameBuffer[j] = (UInt32)0;
            }
            
        }
	}

    //update the peak amp on the instance
    if (renderedInputData) {
        if (!theNewAMIO.inputAvailable) theNewAMIO.inputAvailable = YES;
        //theNewAMIO.peakAmp = maxamp;
    } else if (theNewAMIO.inputAvailable) {
        theNewAMIO.inputAvailable = NO;
    }

	//dodgy return :)
    return noErr;
    
} //auCallback

/*
static void rioInterruptionListener(void *inClientData, UInt32 inInterruption)
{
	printf("NewAMIO: Session interrupted! --- %s ---", inInterruption == kAudioSessionBeginInterruption ? "Begin Interruption\n" : "End Interruption\n");
    
    if (!initialized) return;
	
	//NewAMIO *theNewAMIO = (NewAMIO*)inClientData;
	
	if (inInterruption == kAudioSessionEndInterruption) {
        NewAMIO *theNewAMIO = (NewAMIO *)inClientData;
        
		// make sure we are again the active session
		AudioSessionSetActive(true);
        
        [theNewAMIO _setupAV];
        [theNewAMIO _setupAU];
        
        [theNewAMIO.carrierSampleFile reset];
    
        //AudioOutputUnitStart(outputAudioUnit);
	}
	
	if (inInterruption == kAudioSessionBeginInterruption) {
		AudioOutputUnitStop(outputAudioUnit);
    }
}
*/

-(void)_setupSession
{
	OSStatus status;

    if (status = AudioSessionSetActive(true)) {
        NSLog(@"ERROR: couldn't set audio session active %i", status);
    }
    
    #ifdef __IPHONE_3_0
    #else
    UInt32 doChangeDefaultRoute = 1;
    AudioSessionSetProperty (
        kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,
        sizeof (doChangeDefaultRoute),
        &doChangeDefaultRoute
    );    
    #endif

    if (status = AudioSessionAddPropertyListener(
        kAudioSessionProperty_AudioRouteChange,
        propListener,
        self
    )) {
        NSLog(@"ERROR: couldn't set property listener for session %i", status);
    }

    UInt32 isAvailable = 0;
    UInt32 size = sizeof(isAvailable);
    // we do not want to allow recording if input is not available
    if (status = AudioSessionGetProperty(
        kAudioSessionProperty_AudioInputAvailable,
        &size,
        &isAvailable
    )) {
        NSLog(@"ERROR: couldn't get input available property for session %i", status);
    }
    self.inputAvailable = (isAvailable) ? YES : NO;
}

// Below code is a cut down version (for output only) of the code written by
// Micheal "Code Fighter" Tyson (punch on Mike)
// See http://michael.tyson.id.au/2008/11/04/using-remoteio-audio-unit/ for details
-(void)_setupAU
{
    NSLog(@"NewAMIO: _setupAU");
	OSStatus status;

    /*
    UInt32 doChangeDefaultRoute = 1;
    if (status = AudioSessionSetProperty (
        kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,
        sizeof (doChangeDefaultRoute),
        &doChangeDefaultRoute
    )) {
        NSLog(@"ERROR: couldn't set override to speaker output for session %i", status);
    }
    */
    
    //XThrowIfError(AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, propListener, self), "couldn't set property listener");
    
    /*
    Float32 preferredBufferSize = 0.005;
    if (status = AudioSessionSetProperty(kAudioSessionProperty_PreferredHardwareIOBufferDuration, sizeof(preferredBufferSize), &preferredBufferSize)) {
        NSLog(@"ERROR: couldn't set i/o buffer duration %i", status);
    }
    */
    
    
    Float64	hwSampleRate;
    UInt32 size = sizeof(hwSampleRate);
    if (status = AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate, &size, &hwSampleRate)) {
        NSLog(@"couldn't get hw sample rate %i", status);
    }
    NSLog(@"NewAMIO: hwSampleRate = %f", hwSampleRate);
	
	// Describe audio component
	AudioComponentDescription outputDesc;
	outputDesc.componentType = kAudioUnitType_Output;
	outputDesc.componentSubType = kAudioUnitSubType_RemoteIO;
	outputDesc.componentFlags = 0;
	outputDesc.componentFlagsMask = 0;
	outputDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
    
    /*
    AudioComponentDescription desc;
    desc.componentType = kAudioUnitType_Output;
    desc.componentSubType = kAudioUnitSubType_RemoteIO;
    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    desc.componentFlags = 0;
    desc.componentFlagsMask = 0;
    */
	
	// Get component
	AudioComponent outputComponent = AudioComponentFindNext(NULL, &outputDesc);
    //AudioComponent outputComponent = AudioComponentFindNext(NULL, &desc);
	
	// Get audio units
	if (status = AudioComponentInstanceNew(outputComponent, &outputAudioUnit)) {
        NSLog(@"ERROR: Could not get a new instance of the audio unit %i", status);
    }
	
	UInt32 flag = 1;
	// Enable IO for playback
    if (status = AudioUnitSetProperty(outputAudioUnit, 
								  kAudioOutputUnitProperty_EnableIO, 
								  kAudioUnitScope_Input, //kAudioUnitScope_Output, 
								  kInputBus, //kOutputBus,
								  &flag, 
								  sizeof(flag))) {
        NSLog(@"ERROR: Could not enable I/O for audio %i", status);
    }
	
	// Set up the playback  callback
	AURenderCallbackStruct pCallbackStruct;
	pCallbackStruct.inputProc = auCallback;
	//set the reference to "self" this becomes *inRefCon in the playback callback
	pCallbackStruct.inputProcRefCon = self;
	
	if (status = AudioUnitSetProperty(outputAudioUnit, 
								  kAudioUnitProperty_SetRenderCallback, 
								  kAudioUnitScope_Input, //kAudioUnitScope_Global, 
								  kOutputBus,
								  &pCallbackStruct,
								  sizeof(pCallbackStruct))) {
        NSLog(@"ERROR: Could not set render callback %i", status);
    }

	// Describe formats
    /*
	outputAudioFormat.mSampleRate			= 44100.00;
	outputAudioFormat.mFormatID			= kAudioFormatLinearPCM;
	outputAudioFormat.mFormatFlags		= kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
	outputAudioFormat.mFramesPerPacket	= 1;
	outputAudioFormat.mChannelsPerFrame	= 2;
	outputAudioFormat.mBitsPerChannel		= 16;
	outputAudioFormat.mBytesPerPacket		= 4;
	outputAudioFormat.mBytesPerFrame		= 4;
    */
    //outputAudioFormat.SetAUCanonical(2, false);
    //inputAudioFormat.SetAUCanonical(2, false);
    
    //inputAudioFormat = outputAudioFormat;
    
	//Apply format
	if (status = AudioUnitSetProperty(outputAudioUnit, 
								  kAudioUnitProperty_StreamFormat, 
								  kAudioUnitScope_Input, 
								  kOutputBus, 
								  &outputAudioFormat, 
								  sizeof(outputAudioFormat))) {
        NSLog(@"ERROR: Could not set input stream format %i", status);
    }
                                  
	if (status = AudioUnitSetProperty(outputAudioUnit, 
								  kAudioUnitProperty_StreamFormat, 
								  kAudioUnitScope_Output, 
								  kInputBus, 
								  &inputAudioFormat, 
								  sizeof(inputAudioFormat))) {
        NSLog(@"ERROR: Could not set input output format %i", status);
    }
	 
	 
	// Initialise
	if (status = AudioUnitInitialize(outputAudioUnit)) {
        NSLog(@"ERROR: Could not initialize audio unit %i", status);
    }
    
    /**********************************************************/
    /*
	// Describe audio component
	AudioComponentDescription inputDesc;
	inputDesc.componentType = kAudioUnitType_Output;
	inputDesc.componentSubType = kAudioUnitSubType_RemoteIO;
	inputDesc.componentFlags = 0;
	inputDesc.componentFlagsMask = 0;
	inputDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
	
	// Get component
	AudioComponent inputComponent = AudioComponentFindNext(NULL, &inputDesc);
	
	// Get audio units
	status = AudioComponentInstanceNew(inputComponent, &inputAudioUnit);
	
	flag = 1;
	// Enable IO for playback
	status = AudioUnitSetProperty(inputAudioUnit, 
								  kAudioInputUnitProperty_EnableIO, 
								  kAudioUnitScope_Input, 
								  kInputBus,
								  &flag, 
								  sizeof(flag));

	// Describe formats
    inputAudioFormat.mFormatID          = kAudioFormatLinearPCM;
    inputAudioFormat.mFormatFlags       = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
	inputAudioFormat.mFramesPerPacket	= 1;

	//Apply record format
	status = AudioUnitSetProperty(inputAudioUnit, 
								  kAudioUnitProperty_StreamFormat, 
								  kAudioUnitScope_Input, 
								  kInputBus, 
								  &inputAudioFormat, 
								  sizeof(inputAudioFormat));

	// Set up the record  callback
	AURenderCallbackStruct rCallbackStruct;
	rCallbackStruct.inputProc = recordCallback;
	//set the reference to "self" this becomes *inRefCon in the playback callback
	rCallbackStruct.inputProcRefCon = self;
	
	status = AudioUnitSetProperty(inputAudioUnit, 
								  kAudioUnitProperty_SetRenderCallback, 
								  kAudioUnitScope_Global, 
								  kInputBus,
								  &rCallbackStruct, 
								  sizeof(rCallbackStruct));
	 
	// Initialise
	status = AudioUnitInitialize(inputAudioUnit);
	
	//notice i do nothing with status, i should error check.
    */
}

- (void)setCarrierTone:(BOOL)state
{
    enableCarrierTone = state;
}

//heavily based on work by Aran Mulholland, see AMIOAudioSample.h
-(OSStatus)start{
	OSStatus status = AudioOutputUnitStart(outputAudioUnit);
    
	return status;
}

//heavily based on work by Aran Mulholland, see AMIOAudioSample.h
-(OSStatus)stop{
	OSStatus status = AudioOutputUnitStop(outputAudioUnit);
	return status;
}

void propListener(	void *                  inClientData,
					AudioSessionPropertyID	inID,
					UInt32                  inDataSize,
					const void *            inData)
{
    NewAMIO *theNewAMIO = (NewAMIO *)inClientData;
    
    printf("NewAMIO: propListener ", inID, "\n");
    
	if (inID == kAudioSessionProperty_AudioRouteChange)
	{
		CFDictionaryRef routeDictionary = (CFDictionaryRef)inData;			
		//CFShow(routeDictionary);
		CFNumberRef reason = (CFNumberRef)CFDictionaryGetValue(routeDictionary, CFSTR(kAudioSession_AudioRouteChangeKey_Reason));
		SInt32 reasonVal;
		CFNumberGetValue(reason, kCFNumberSInt32Type, &reasonVal);
		if (reasonVal != kAudioSessionRouteChangeReason_CategoryChange)
		{
			/*CFStringRef oldRoute = (CFStringRef)CFDictionaryGetValue(routeDictionary, CFSTR(kAudioSession_AudioRouteChangeKey_OldRoute));
			if (oldRoute)	
			{
				printf("old route:\n");
				CFShow(oldRoute);
			}
			else 
				printf("ERROR GETTING OLD AUDIO ROUTE!\n");
			
			CFStringRef newRoute;
			UInt32 size; size = sizeof(CFStringRef);
			OSStatus error = AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &size, &newRoute);
			if (error) printf("ERROR GETTING NEW AUDIO ROUTE! %d\n", error);
			else
			{
				printf("new route:\n");
				CFShow(newRoute);
			}*/

            /*
			if (reasonVal == kAudioSessionRouteChangeReason_OldDeviceUnavailable)
			{			
				if (theNewAMIO->player->IsRunning()) {
					[theNewAMIO pausePlayQueue];
					[[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueStopped" object:theNewAMIO];
				}		
			}
            */

			// stop the queue if we had a non-policy route change
            /*
			if (theNewAMIO->recorder->IsRunning()) {
				[theNewAMIO stopRecord];
			}*/
		}	
	}
	else if (inID == kAudioSessionProperty_AudioInputAvailable)
	{
		if (inDataSize == sizeof(UInt32)) {
			UInt32 isAvailable = *(UInt32*)inData;
			// update the inputAvailable property
            printf("NewAMIO: propListener: changed, inputAvailable = %i", isAvailable);
			theNewAMIO->inputAvailable = (isAvailable > 0) ? YES : NO;
		}
	}
}



@end
